Skip to content

[eslint-miner] eslint: add require-fs-io-try-catch rule (statSync / readdirSync / copyFileSync / unlinkSync / renameSync) - #47259

Merged
pelikhan merged 3 commits into
mainfrom
eslint-miner/require-fs-io-try-catch-ba31b9f32b87928e
Jul 22, 2026
Merged

[eslint-miner] eslint: add require-fs-io-try-catch rule (statSync / readdirSync / copyFileSync / unlinkSync / renameSync)#47259
pelikhan merged 3 commits into
mainfrom
eslint-miner/require-fs-io-try-catch-ba31b9f32b87928e

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new ESLint rule require-fs-io-try-catch to the eslint-factory plugin. The rule enforces that synchronous filesystem I/O methods — statSync, readdirSync, copyFileSync, unlinkSync, and renameSync — are wrapped in try/catch blocks in actions/setup/js scripts.

These methods throw synchronously on missing files, permission errors (EACCES), busy resources (EBUSY), and other I/O failures. An unhandled throw crashes the GitHub Action without surfacing a useful diagnostic message.

Changes

File Change
eslint-factory/src/rules/require-fs-io-try-catch.ts New rule implementation using shared try-catch-rule-utils helpers
eslint-factory/src/rules/require-fs-io-try-catch.test.ts Vitest + RuleTester test suite covering valid/invalid cases, destructured imports, scope exclusions
eslint-factory/src/index.ts Registered requireFsIoTryCatchRule in the plugin rules registry
eslint-factory/eslint.config.cjs Enabled rule at warn severity in the plugin config
eslint-factory/README.md Added rule documentation with detected forms, out-of-scope cases, and safe alternative

Rule behaviour

Detected forms:

  • fs.statSync(path) — member expression on a known require("fs") binding
  • fs["readdirSync"](dir) — computed string-literal property access
  • const { unlinkSync } = require("fs") — destructured CJS binding
  • import * as fs from "fs"; fs.copyFileSync(src, dest) — ESM namespace import
  • import { renameSync } from "fs"; renameSync(src, dest) — ESM named import
  • statSync(path) — bare unbound identifier (not locally declared)

Out of scope: non-fs/node:fs sources (e.g. mockFs.statSync); existsSync; readFileSync/writeFileSync/appendFileSync (covered by require-fs-sync-try-catch).

Suggestion fixer: wraps the enclosing statement in try { ... } catch (err) { throw new Error("fs.<method> failed: ...", { cause: err }); } with preserved indentation.

Breaking changes

None. The rule is added at warn severity; no existing valid code is flagged.

Generated by PR Description Updater for #47259 · sonnet46 37.1 AIC · ⌖ 7.52 AIC · ⊞ 4.8K ·

…yFileSync/unlinkSync/renameSync

Add a new custom ESLint rule that flags fs.statSync, fs.readdirSync,
fs.copyFileSync, fs.unlinkSync, and fs.renameSync calls in
actions/setup/js when they are not wrapped in try/catch.

These methods are the next-highest-risk group of synchronous fs calls
after readFileSync/writeFileSync/appendFileSync (already covered by
require-fs-sync-try-catch). A scan of actions/setup/js found 33
unguarded call sites across files including artifact_client.cjs,
check_workflow_timestamp.cjs, comment_memory_helpers.cjs,
merge_remote_agent_github_folder.cjs, and send_otlp_span.cjs.

All five methods throw synchronously on ENOENT, EACCES, EBUSY, etc.
Without a try/catch, the error propagates as an unhandled exception
that crashes the action step with no useful diagnostic message.

The rule reuses the createFsSyncMethodResolver / isInsideTryBlock
helpers from try-catch-rule-utils so resolver coverage (fs import,
destructured bindings, computed member access) is consistent with the
existing rule family.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! eslint labels Jul 22, 2026
@pelikhan
pelikhan marked this pull request as ready for review July 22, 2026 09:59
Copilot AI review requested due to automatic review settings July 22, 2026 09:59
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ PR Code Quality Reviewer failed to deliver outputs during code quality review.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (0 additions detected in default business directories: src/, lib/, pkg/, internal/, app/, core/, domain/, services/, api/).

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an ESLint rule requiring try/catch protection around five synchronous filesystem operations in action setup scripts.

Changes:

  • Detects unguarded filesystem I/O calls and offers fixes.
  • Adds rule tests and plugin registration.
  • Enables the rule at warning severity.
Show a summary per file
File Description
eslint-factory/src/rules/require-fs-io-try-catch.ts Implements detection and suggestions.
eslint-factory/src/rules/require-fs-io-try-catch.test.ts Tests supported methods and bindings.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables rule warnings.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Medium

const FS_IO_METHODS = new Set(["statSync", "readdirSync", "copyFileSync", "unlinkSync", "renameSync"]);

export const requireFsIoTryCatchRule = createRule({
name: "require-fs-io-try-catch",

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean implementation that follows the established require-execsync-try-catch pattern. The rule correctly targets the 5 throwing fs methods, delegates to shared resolver utilities, and the test coverage is thorough. LGTM.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 13.7 AIC · ⌖ 5.08 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

Analyzed 9 test(s): 9 design, 0 implementation, 0 violation(s).

📊 Metrics (9 tests)
Metric Value
Analyzed 9 (TypeScript/vitest: 9)
✅ Design 9 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 7 (78%)
Duplicate clusters 1 (methods 3-7 follow same pattern, acceptable for parameterized ESLint rules)
Inflation (test:prod lines) 1.64:1 ✅
🚨 Violations 0
✅ Test Coverage Summary

Valid cases (no false positives):

  • statSync/readdirSync/copyFileSync/unlinkSync/renameSync inside try/catch → pass ✅
  • Out-of-scope methods (existsSync, readFileSync, writeFileSync) → correctly ignored ✅
  • Destructured imports in try/catch → pass ✅

Invalid cases (violations caught):

  • Unguarded statSync, readdirSync, copyFileSync, unlinkSync, renameSync → flagged with exact method and arg ✅
  • Destructured methods outside try/catch → flagged ✅

Edge cases:

  • Destructured node:fs vs fs imports (test 8)
  • Correct error metadata extraction (messageId + data with method/arg)

Verdict

Passed. All 9 tests are design tests. 0% implementation tests (threshold: 30%). No coding violations. Test inflation acceptable (1.64:1 < 2:1).

Analysis: Tests comprehensively cover the rule's scope using ESLint's RuleTester framework. Each test verifies user-visible behavior: correct code doesn't trigger false positives, violations are caught with exact error messages, and destructured imports are handled correctly. Pattern repetition in tests 3-7 (one method per test) is a deliberate and acceptable practice for ESLint rule testing to ensure individual method coverage.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 10.8 AIC · ⌖ 7.78 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 70/100. 0% implementation tests (threshold: 30%). All 9 tests verify user-visible behavior through ESLint's RuleTester framework: correct code passes without false positives, violations are caught with exact error metadata, and edge cases (destructured imports) are properly handled.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design and /tdd — commenting with suggestions, no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Code duplication: The implementation is a near-exact copy of require-fs-sync-try-catch.ts. As a third (or fourth) rule of this shape appears, maintenance cost will compound. A shared factory would be worth the one-time investment now.
  • Test coverage gaps: Destructured-call tests cover only statSync/readdirSync; the other three methods are untested in that path.
  • Test structure asymmetry: Valid cases are grouped into one block while invalid cases are split by method — a minor inconsistency that could hide regressions.

Positive Highlights

  • ✅ Good choice to use the existing createFsSyncMethodResolver / isInsideTryBlock infrastructure rather than reinventing it.
  • ✅ The PR description is clear and well-evidenced with 33 concrete call sites.
  • ✅ Rule is registered as warn not error, which is appropriate for a linter rollout where existing call sites haven't been fixed yet.
  • ✅ Test file follows the established per-method pattern from the sibling rule.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 28.4 AIC · ⌖ 4.88 AIC · ⊞ 6.7K
Comment /matt to run again


// fs methods beyond readFileSync/writeFileSync/appendFileSync that throw on I/O failure
// and appear frequently unguarded in actions/setup/js.
const FS_IO_METHODS = new Set(["statSync", "readdirSync", "copyFileSync", "unlinkSync", "renameSync"]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] This rule is near-identical to require-fs-sync-try-catch.ts — same AST visitor, same fixer logic, same message strings, only the method set differs. Consider extracting a shared factory to avoid two parallel implementations drifting apart.

💡 Suggested refactor

Add a small factory in try-catch-rule-utils.ts:

export function createFsMethodTryCatchRule(
  name: string,
  methods: Set<string>,
  description: string
) {
  return createRule({ name, meta: { ... description ... }, create(context) { /* shared visitor */ } });
}

Both existing rules then become one-liners, and any future rule follows the same pattern at zero cost.

@copilot please address this.

],
invalid: [],
});
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The valid tests group all five methods in one it block, but the invalid tests use one it per method. This asymmetry means a regression where a method is silently dropped from the valid set would only fail if you happened to test that one. Consider mirroring the structure — one combined invalid block, or one valid block per method — so coverage is symmetric.

@copilot please address this.

},
],
});
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The destructured-valid test covers statSync and readdirSync but not copyFileSync, unlinkSync, or renameSync. Similarly the destructured-invalid test only covers statSync. A regression that broke destructuring detection for the other three methods would go undetected.

💡 Add missing destructured cases
valid: [
  `const { statSync } = require("node:fs"); try { statSync(path); } catch (e) {}`,
  `const { copyFileSync } = require("fs"); try { copyFileSync(src, dest); } catch (e) {}`,
  `const { unlinkSync } = require("fs"); try { unlinkSync(path); } catch (e) {}`,
  `const { renameSync } = require("fs"); try { renameSync(a, b); } catch (e) {}`,
],
invalid: [
  { code: `const { copyFileSync } = require("fs"); copyFileSync(src, dest);`, errors: [{ messageId: "requireTryCatch" }] },
  { code: `const { unlinkSync } = require("fs"); unlinkSync(path);`, errors: [{ messageId: "requireTryCatch" }] },
],

@copilot please address this.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot merge main and recompile

Copilot AI and others added 2 commits July 22, 2026 11:06
…fs-io-try-catch-ba31b9f32b87928e

# Conflicts:
#	eslint-factory/eslint.config.cjs
#	eslint-factory/src/index.ts

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@copilot merge main and recompile

Done. Merged origin/main into the branch (resolved conflicts in eslint-factory/src/index.ts and eslint-factory/eslint.config.cjs — kept both require-fs-io-try-catch from this branch and no-setfailed-then-exit-zero from main). Also added the missing require-fs-io-try-catch documentation to eslint-factory/README.md and updated the stale out-of-scope note in the require-mkdirsync-try-catch section.

Copilot AI requested a review from pelikhan July 22, 2026 11:10
@pelikhan
pelikhan merged commit 2ac3b25 into main Jul 22, 2026
@pelikhan
pelikhan deleted the eslint-miner/require-fs-io-try-catch-ba31b9f32b87928e branch July 22, 2026 11:20
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.83.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! eslint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants